viewChild
函數的一個高階用例是在 ViewConatinerRef
中建立 Angular 組件。。當我們知道應用程式載入期間不需要 Angular 組件時,我們可以將其延遲載入。優點是主包較小且初始載入時間較快。
在示範中,我以程式設計方式 (programmatically) 建立組件來顯示 jedi warriors 和 sith lords。 此示範使用 viewChild
函數查詢 ViewContainerRef
並呼叫其 createComponent
方法來動態建立組件。
import { provideExperimentalZonelessChangeDetection } from '@angular/core';
import { provideHttpClient } from '@angular/common/http';
export const appConfig = {
providers: [
provideHttpClient(),
provideExperimentalZonelessChangeDetection()
]
}
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app.config';
bootstrapApplication(App, appConfig);
提供 Http client 和 experimental zoneless 功能,並引導應用程式設定。
import { catchError, map, of, mergeMap, forkJoin } from 'rxjs';
import { inject, runInInjectionContext, Injector } from '@angular/core';
import { HttpClient } from '@angular/common/http';
export type Person = {
name: string;
height: string;
mass: string;
hair_color: string;
skin_color: string;
eye_color: string;
gender: string;
films: string[];
}
const URL = 'https://swapi.dev/api/people';
export function getPerson(id: number, injector: Injector) {
return runInInjectionContext(injector, () => {
const http = inject(HttpClient);
return http.get<Person>(`${URL}/${id}`).pipe(
catchError((err) => {
console.error(err);
return of(undefined);
}));
});
}
getPerson
函數透過 id 檢索星際大戰角色。
import { ChangeDetectionStrategy, Component, effect, inject, Injector, model, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { getPerson, Person } from './star-war.api';
@Component({
selector: 'app-star-war-character',
standalone: true,
imports: [FormsModule],
template: `
<div class="border">
@if(person(); as person) {
<p>Id: {{ id() }} </p>
@if (isSith()) {
<p>A Sith, he is evil.</p>
}
<p>Name: {{ person.name }}</p>
<p>Height: {{ person.height }}</p>
<p>Mass: {{ person.mass }}</p>
<p>Hair Color: {{ person.hair_color }}</p>
<p>Skin Color: {{ person.skin_color }}</p>
<p>Eye Color: {{ person.eye_color }}</p>
<p>Gender: {{ person.gender }}</p>
} @else {
<p>No info</p>
}
</div>
`,
})
export class AppStarWarCharacterComponent {
injector = inject(Injector);
id = model(1);
isSith = model(false);
person = signal<undefined | Person>(undefined);
constructor() {
effect((onCleanUp) => {
const sub = getPerson(this.id(), this.injector)
.subscribe((result) => {
if (result) {
const [person] = result;
this.person.set(person);
} else {
this.person.set(undefined);
}
});
onCleanUp(() => sub.unsubscribe());
});
}
}
AppStarWarCharacterComponent
有兩個 model inputs
:id
和 isSith
。 此組件使用 id
model input 呼叫 Star War API 來檢索資料並將結果指派給 person
signal。 當 isSith
signal 為 true
時,它顯示 "A Sith, he is evil"。
Component({
selector: 'app-root',
standalone: true,
imports: [FormsModule],
template: `
<div class="container">
<ng-container #vcr />
</div>
<select [(ngModel)]="jediId">
<option value="1">Luke</option>
<option value="10">Obi Wan Kenobe</option>
<option value="20">Yoda</option>
</select>
<button (click)="addAJedi(jediId())">Add a Jedi</button>
<select [(ngModel)]="sithId">
<option value="4">Darth Vader</option>
<option value="44">Darth Maul</option>
</select>
<button (click)="addAJedi(sithId(), true)">Add a Sith</button>`,
})
export class App implements OnDestroy {
jediId = signal(1);
sithId = signal(4);
ngOnDestroy(): void {}
}
App
組件由 Jedi
和 Sith
下拉列表組成。 Jedi
列表的 NgModel
綁定到 jediId
signal, Sith
列表的 NgModel
綁定到 sithId
signal。當使用者點擊 "Add a Jedi" 按鈕時,組件會呼叫 addAJedi
方法將 AppStarWarCharacterComponent
附加到 ViewContainerRef
。同樣,使用者點擊 "Add a Sith" 按鈕來呼叫相同的方法,將 AppStarWarCharacterComponent
組件附加到 ViewContainerRef
。
<ng-container #vcr />
NgContainer
有一個範本變數 vcr
,viewChild
函數使用它來查詢 ViewContainerRef
。
vcr = viewChild.required('vcr', { read: ViewContainerRef });
vcr 的類型是 Signal<ViewContainerRef>
,因為 read 屬性會擷取 ViewContainerRef
。
componentRefs = [] as ComponentRef<any>[];
async addAJedi(id: number, isSith = false) {
const { AppStarWarCharacterComponent } = await import ('./star-war/star-war-character.component');
AppStarWarCharacterComponent
const componentRef = this.vcr().createComponent(AppStarWarCharacterComponent);
componentRef.instance.id.set(id);
componentRef.instance.isSith.set(isSith);
this.componentRefs.push(componentRef);
}
addJedi
方法首先匯入 AppStarWarCharacterComponent
。然後,createComponent
方法將組件附加到 ViewContainerRef
並傳回 ComponentRef
reference。使用 id
和 isSith
設定 componentRef.instance
的 model input。 將 componentRef
附加到 componentRefs
陣列, 在 ngDestroy
lifecycle hook 中銷毀。
ngOnDestroy(): void {
if (this.componentRefs) {
for (const ref of this.componentRefs) {
ref.destroy();
}
}
}
``
當應用程式銷毀 `App` 組件時,`ngOnDestroy` 會釋放 `componentRefs` 的 memory 以避免 memory leaks。
## 結論:
- `viewChild` 可以查詢 `ViewContainerRef`,並且 `ViewContainerRef` 可以呼叫 `createComponent` 方法以程式設計方式附加組件。
- 組件可以設定 model input 來執行任何邏輯來更新其 HTML 範本。
鐵人賽的第 18 天就這樣結束了。
## 參考:
- Viewchild as signal: https://angular.dev/guide/signals/queries#viewchild
- Render a component programmatically: https://angular.dev/guide/components/programmatic-rendering#
- Demo: https://stackblitz.com/edit/stackblitz-starters-zuyqrt?file=src%2Fmain.ts